#pragma once

#include "common.hpp"
#include "logdef.hpp"

#include <ctime>
#include <fstream>
#include <ostream>

/**
 * @brief A placeholder class for end of log output
 */
struct logger_end {};

static logger_end lend;

/**
 * @brief the logger class for the project
 *
 * use a singleton to maintain the state of the logger.
 */
struct logger {
    static constexpr size_t MAX_LOG_FILE_SIZE = 1ull << 20l;

    static logger& get_instance() {
        static logger l;
        return l;
    }

    ~logger() {
        std::lock_guard<std::mutex> l(m);
    }

    void set_output_file_path(std::string path) {
        if (path.empty()) {
            output_to_file = false;
        } else {
            output_to_file = true;
            log_file_path = std::move(path);
            if (!std::filesystem::exists(log_file_path)) {
                std::filesystem::create_directories(log_file_path);
            }
        }
    }

    void set_output_to_stdout(bool flag) {
        output_to_stdout = flag;
    }

private:
    bool output_to_file;
    bool output_to_stdout{true};
    std::string log_file_path;

    logger(bool output_to_file = false, std::string log_path = "./log")
        : output_to_file(output_to_file)
        , log_file_path(std::move(log_path)) {
        if (output_to_file && !std::filesystem::exists(log_file_path)) {
            std::filesystem::create_directories(log_file_path);
        }
    }

    std::stringstream buffer;
    std::ofstream fout;
    size_t log_file_size{0};

public:
    std::mutex m;

    /**
     * @brief init the logger
     *
     * create the output file, get current time and set thread id
     */
    void init() {
        if (output_to_file && (!fout.is_open() || log_file_size > MAX_LOG_FILE_SIZE)) {
            auto now = now_in_i64();
            fout.open(log_file_path + "/" + std::to_string(now) + ".log", std::ios::app | std::ios::out);
        }
        buffer.clear();
        auto now = std::chrono::system_clock::now();
        auto in_time_t = std::chrono::system_clock::to_time_t(now);

        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
        struct tm new_time {};
        ::localtime_s(&new_time, &in_time_t);
        buffer << "[" << std::put_time(&new_time, "%Y-%m-%d %X") << '.' << std::setfill('0') << std::setw(3)
               << ms.count() << "]";

        buffer << "[" << std::this_thread::get_id() << "]";
    }

    /**
     * @brief output operator for logger
     *
     * @tparam T the type which is acceptable by std::stringstream::opreator<<
     * @param content the output content
     * @return logger& the logger itself
     */
    template <typename T>
    logger& operator<<(const T& content) {
        buffer << content;
        return *this;
    }

    /**
     * @brief the end of an output process
     *
     * to output contents to file or stdout and release the mutex
     */
    void operator<<(const logger_end&) {
        auto str = buffer.str();
        buffer.str("");
        if (output_to_stdout) {
            std::cout << str << "\n";
        }
        if (output_to_file) {
            fout << str << std::endl;
            log_file_size += str.size();
        }
        m.unlock();
    }
};

/**
 * @brief get logger and lock it up
 *
 * @return logger& the logger reference
 */
inline logger& logger_() {
    logger& l = logger::get_instance();
    l.m.lock();
    l.init();
    return l;
}

#ifndef log_level
#define log_level log_trace
#endif

#define log_template(LEVEL, MSG) logger_() << "[" __FILE__ ":" << __LINE__ << "][" #LEVEL "] " << MSG << lend

#if log_level <= log_trace
#define ltrace(MSG) log_template(trace, MSG)
#else
#define ltrace(MSG)
#endif

#if log_level <= log_debug
#define ldebug(MSG) log_template(debug, MSG)
#else
#define ldebug(MSG)
#endif

#if log_level <= log_info
#define linfo(MSG) log_template(info, MSG)
#else
#define linfo(MSG)
#endif

#if log_level <= log_warn
#define lwarn(MSG) log_template(warn, MSG)
#else
#define lwarn(MSG)
#endif

#if log_level <= log_error
#define lerror(MSG) log_template(error, MSG)
#else
#define lerror(MSG)
#endif

#if log_level <= log_fatal
#define lfatal(MSG) log_template(fatal, MSG)
#else
#define lfatal(MSG)
#endif

#define assert(COND, MSG)                                                            \
    [&]() {                                                                          \
        if (!(COND)) {                                                               \
            lfatal(MSG << " at " << nonstd::to_string(std::wstring(__FUNCTIONW__))); \
            exit(1);                                                                 \
        }                                                                            \
    }()